import json
from kdtree import *
from extent import *
from rtree import *
import cProfile_performance_test

# класс объекта для хранения значений из geojson файла
class Object:
    def __init__(self, id = None, geometry = None, MBR = None):
        self.id = id
        self.geometry = geometry
        self.MBR = MBR

def get_object_MBR(objects_coordinates): # получение MBR из координат объекта
    xmin, xmax = objects_coordinates[0][0], objects_coordinates[0][0] 
    ymin, ymax = objects_coordinates[0][1], objects_coordinates[0][1]

    if len(objects_coordinates) == 1: # если объект точка, то возвращаем MBR
        return [xmin, xmax, ymin, ymax]
    # Иначе идём по массиву координат и соотавляем MBR для полилинии
    for coordinate in objects_coordinates:
        xmin = min(coordinate[0], xmin)
        xmax = max(coordinate[0], xmax)
        ymin = min(coordinate[1], ymin)
        ymax = max(coordinate[1], ymax)

    return [xmin, xmax, ymin, ymax]

def get_objects(): # получение объектов из переданного файла
    file_name = input('Введите полное имя файла: ')

    try:
        with open(file_name, 'r') as file:
            geojson_data = json.load(file)
    except:
        raise FileNotFoundError

    return geojson_data

def get_objects_list_for_kdtree(geojson_data): # проход по переданным объектам для построения kdtree
    objects_list = [] # массив объектов
    objects_count = 0 # переменная для присвоения id объектам

    for item in geojson_data['features']:
        if item['geometry']['type'] == 'Point': # если объект - точка, то просто добавляем его в массив
            new_object = Object(objects_count, item['geometry'])
            objects_list.append(new_object)

        if item['geometry']['type'] == 'LineString': # если объект - полилиния, то
            for value in item['geometry']['coordinates']: # проходим по каждой точке полилинии и отдельно добавляем в массив
                new_object = Object(objects_count, { 'type' : item['geometry']['type'], 'coordinates' : value })
                objects_list.append(new_object)
        # стоит отметить, что все точки полилинии будут иметь одинаковый id, но разные координаты
        objects_count += 1
    
    return objects_list

def get_objects_list_for_rtree(geojson_data): # проход по переданным объектам для построения rtree
    objects_list = []
    objects_count = 0
    # по факту, всё то же самое, что и в предыдущей функции, но здесь полилинии добавляются полностью, а не отдельными точками
    # Также, у всех объектов есть MBR
    for item in geojson_data['features']:
        if item['geometry']['type'] == 'Point':
            MBR = get_object_MBR([item['geometry']['coordinates']])
        elif item['geometry']['type'] == 'LineString':
            MBR = get_object_MBR(item['geometry']['coordinates'])
        
        new_object = Object(objects_count, item['geometry'], MBR)
        objects_list.append(new_object)

        objects_count += 1
    
    return objects_list

def get_defined_object(kdtree): # Получение значений заданного объекта
    print('\nВыберите тип геометрии для ввода:\n1. Point\n2. LineString\n')
    object_type = input('Тип геометрии: ')

    try:
        object_type = int(object_type)
    except:
        raise ValueError

    defined_objects = [] # массив для храниения значений

    if object_type == 1:
        x = float(input('Введите значение точки по широте (x): '))
        y = float(input('Введите значение точки по высоте (y): '))
        defined_objects.append([x, y])
    elif object_type == 2:
        points_count = int(input('Введите количество точек в полилинии: '))
        for i in range(points_count):
            x = float(input(f'\tВведите значение {i+1} точки по широте (x): '))
            y = float(input(f'\tВведите значение {i+1} точки по высоте (y): '))
            defined_objects.append([x, y])
    else:
        raise IndexError
    # если флаг kdtree истинный, то просто возвращаем массив со всеми значениями координат
    if kdtree:
        return defined_objects
    # если же флаг ложный, то значит работа будет идти с rtree
    MBR = get_object_MBR(defined_objects) # получение ограничивающего прямоугольника для введённого объекта
    # возвращаем объект с ограничивающим прямоугольником
    return Object(geometry = defined_objects, MBR = Extent(MBR[0], MBR[1], MBR[2], MBR[3]))

def get_distance(point1, point2): # получение расстояния между точками в метрах
    lat1, long1 = (float(point1[0]) * math.pi / 180), (float(point1[1]) * math.pi / 180) # получение широты и долготы первой точки
    lat2, long2 = (float(point2[0]) * math.pi / 180), (float(point2[1]) * math.pi / 180) # получение широты и долготы второй точки

    lat1_cos, lat1_sin = math.cos(lat1), math.sin(lat1) # получение значений косинуса и синуса по широте 1 точки
    lat2_cos, lat2_sin = math.cos(lat2), math.sin(lat2) # получение значений косинуса и синуса по широте 2 точки
    delta = long2 - long1 # разница между долготами точек
    delta_cos, delta_sin = math.cos(delta), math.sin(delta) # синус и косинус полученной разницы

    y = math.sqrt(math.pow(lat2_cos * delta_sin, 2) + math.pow(lat1_cos * lat2_sin - lat1_sin * lat2_cos * delta_cos, 2))
    x = lat1_sin * lat2_sin + lat1_cos * lat2_cos * delta_cos  
    # Радиус Земли = 6372795 метров
    # получение расстояния в метрах при помощи умножения на значение радиуса земли в метрах
    distance = round(math.atan2(y, x) * 6372795)

    return distance

def get_distance_for_defined_point(objects, defined_object): # дистанция между объектами и заданной точкой
    distance_list = []
    # проход по всем объектам
    for ent in objects: 
        if ent.geometry['type'] == 'Point': # если текущий объект точка, то вычисляем дистанцию и добавляем её в массив
            distance_list.append(get_distance(ent.geometry['coordinates'], defined_object))
        else: # инчае
            min_distance_for_polyline = [] # создаем дополнительный массив
            for geometry in ent.geometry['coordinates']: # проход по точками полилинии
                min_distance_for_polyline.append(get_distance(geometry, defined_object)) # вычисление дистанции
            min_distance_for_polyline = min(min_distance_for_polyline, key = lambda distance : distance) # получение минимальной дистанции
            distance_list.append(min_distance_for_polyline) # добавление в массив
     
    return distance_list # возвращение полученного массива с растояниями от заданной точки до остальных объектов

def get_distance_for_defined_polyline(objects, defined_object):
    distance_list = []
    # проход по всем точкам полилинии
    for point in defined_object.geometry: 
        distance_list.append(min(get_distance_for_defined_point(objects, point))) # получение минимальной дистанции к точке через функцию get_distance for point

    return distance_list

def get_distance_between_objects(objects, defined_object): # Если длина геометрии равна 1, то это точка
    if len(defined_object.geometry) == 1:
        return get_distance_for_defined_point(objects, defined_object.geometry[0])
    else: # иначе - полилиния
        return get_distance_for_defined_polyline(objects, defined_object)

def main():
    geojson_data = get_objects() # получение данных из файла

    M = int(input('Введите количество объектов для поиска: ')) # количество ближайших объектов, которое необходимо найти

    
    if M == 1: # если надо найти только 1 объект
        defined_object = get_defined_object(True) # ввод пользователя с флагом True, чтобы передать только значения геометрии
        objects_list = get_objects_list_for_kdtree(geojson_data) # получение массива объектов для kdtree
        root = build_kdtree(objects_list) # создание rdtree
        
        closest_objects = [] # массив для ближайших объектов к заданному

        for value in defined_object: # прохождение по всем точкам заданного объекта и последующее добваление в массив ближайшего к нему
            closest_objects.append(kdtree_closest_point(root, value))
        # получение объекта с минимальным расстоянием до заданного
        finded_node = get_closest_object_from_kdtree(closest_objects)
        finded_node['geometry']['coordinates'] = get_all_line_values_from_kdtree(root, finded_node, [])
        # вывод
        print(f"\nID - { finded_node['id'] } | Тип - { finded_node['geometry']['type'] } | Дистанция до заданного объекта в метрах: { finded_node['distance'] }\
            \nКоординаты: { finded_node['geometry']['coordinates'] }")
    elif M > 1: # если надо найти больше 1 объекта, то работа п=будет идти с rtree
        defined_object = get_defined_object(False)  # ввод пользователя с флагом False, чтобы передать геометрию и MBR
        objects_list = get_objects_list_for_rtree(geojson_data) # получение массива объектов для rtree
        root = RtreeNode(M+1) # создание дерева с размером узла М+1, так как количество объектов в узле будет M-1
        # получение MBR всех объектов
        extents = [Extent(obj.MBR[0], obj.MBR[1], obj.MBR[2], obj.MBR[3], obj.id, obj.geometry) for obj in objects_list]
        # добавление объектов в rtree
        for e in extents:
            n = search_rtree_extent(root, e)
            insert(n, e)
        # поиск по rtree
        finded_node = search_rtree_extent(root, defined_object.MBR)
        # массив с дистанцией до заданного объекта
        distance_list = get_distance_between_objects(finded_node.entries, defined_object)
        # вывод
        print(f"\nБлижайшие объекты по отношеннию к заданному объекту:")
        for i, ent in enumerate(finded_node.entries):
            print(f"\nID - { ent.id } | Тип - { ent.geometry['type'] } | Дистанция до заданного объекта в метрах: { distance_list[i] }\
                \nКоординаты: { ent.geometry['coordinates'] }")
    else:
        print("Количество объектов для поиска не может быть меньше 1!")

    print("\n\n")

if __name__ == '__main__':
    bench = input("Желаете провести бенчмарк программы? (y/n): ")

    if bench == "y":
        cProfile_performance_test.performance_info("main")
    else:
        main()